浅谈Web Worker关闭的问题

发布于 · 最后修改时间

Web Worker是没有提供onclose事件的,但它有提供terminate函数。
可能官方很自信地觉得Worker只要是用户销毁的,那么就没必要onclose……但其实昨天就遇到这个问题了(在Cordova-Ionic-Webview里头),就是从后台唤起程序,WebWorker没响应了,被杀了……在调试控制台已经看不到这个Worker的身影。
解决办法我想有三个:

  1. 原生层面入手,去监控有什么系统层面的回调会触发
  2. 改成用ServiceWorker试一下
  3. 监控WebWorker的销毁

为了简单且通用起见,我先选择了3。
但其实在官方接口里头是没有相关的接口的,这就只能另辟蹊径。
一开始我想到的是研究MessageChannel。因为从接口层面来说,它们几乎是一出的,也许底层实现是一样的。
所以就去研究如何识别MessageChannel是close状态的。最糟糕的方式估计就是pingpong,但这就得额外增加脏代码。
后来忽然想到transferable这个标准,所以就有了以下的骚操作:

const b = new ArrayBuffer(1);
port1.postMessage(0,[b]);
console.log(b.byteLength);

如果MessagePort是开启的状态,内存对象会被顺利传输,从而打印“0”。否则如果打印“1”,就说明MessagePort已经被关闭。
用这个方法去实验WebWorker。理论上几乎是一个东西吧……事实却是即便WebWorker执行了terminate,ArrayBuffer仍旧会被传输过去……这就很恐怖了,错觉自己是不是遇上了浏览器内存泄漏的问题……一搜索其实github上三四年前就已经有人提出了,到现在仍旧没有音讯。实在不理解terminate居然没有销毁消息管道……那我发送到子进程的ArrayBuffer到底发到哪里了呢?

既然这条路不通,我就再换一个操作。我想terminate至少会销毁WebWorker中所有的Promise吧。故而我想起了有这个一个API:LockManager 。

PS: 我后来测试在使用new Worker('data:text/javascript;,')这样的data:协议下的Worker环境,是不支持的。建议还是用https协议。
它的用法很简单,虽然还是需要在子进程中注入代码,但至少也比pingpong的方案好,毕竟Worker就是要用来执行密集型任务,哪里还能保障一定能返回pingpong呢。所以只要在子进程启动后,执行一个永远不释放的request,然后发一个信号告知主进程开始监听这个request:

/// worker
const lockReqId = 'process-live-'+Date.now()+Math.random();
navigator.locks.request (lockReqId,()=>new Promise(()=>{}));
postMessage(lockReqId);

/// master
worker.addEventListener('message', me=>{
if(typeof me.data==='string' && me.data.startsWith("process-live-")){
navigator.locks.request(me.data,()=>{
worker.dispatchEvent(new CloseEvent('close'))
})
}
});

这个方案几乎是完美的。因为是直接跟js引擎的销毁与否进行绑定。不过它只有chrome69+才支持。


不过pingpong只能识别出Worker不响应,它可能繁忙。如果没有预期中的响应,应该将是否重启WebWorker